package com.springboot.tools.mapper;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

import com.springboot.tools.annotations.IdType;
import com.springboot.tools.handler.MybatisXMLLanguageDriver;
import com.springboot.tools.toolkit.TableFieldInfo;
import com.springboot.tools.toolkit.TableInfo;
import com.springboot.tools.toolkit.TableInfoHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.scripting.defaults.RawSqlSource;
import org.apache.ibatis.session.Configuration;


public class AutoSqlInjector {

    private static final MybatisXMLLanguageDriver languageDriver = new MybatisXMLLanguageDriver();

    private Configuration configuration;

    private MapperBuilderAssistant assistant;

    public AutoSqlInjector(Configuration configuration) {
        this.configuration = configuration;
    }

    public void inject(Class<?> mapperClass) {
        assistant = new MapperBuilderAssistant(configuration, mapperClass
                .getName().replaceAll("\\.", "/"));
        assistant.setCurrentNamespace(mapperClass.getName());

        Class<?> modelClass = extractModelClass(mapperClass);
        TableInfo table = TableInfoHelper.getTableInfo(modelClass);

        if (table.getKeyProperty() != null) {

            this.injectInsertSql(false, mapperClass, modelClass, table);
            this.injectInsertSql(true, mapperClass, modelClass, table);

            this.injectDeleteSelectiveSql(mapperClass, modelClass, table);
            this.injectDeleteSql(false, mapperClass, modelClass, table);
            this.injectDeleteSql(true, mapperClass, modelClass, table);

            this.injectUpdateSql(mapperClass, modelClass, table);
            this.injectSelectSql(false, mapperClass, modelClass, table);
            this.injectSelectSql(true, mapperClass, modelClass, table);
            this.injectSelectOneSql(mapperClass, modelClass, table);
            this.injectSelectListSql(mapperClass, modelClass, table);
        } else {
            System.err
                    .println(String
                            .format("%s ,The unknown primary key, cannot use the generic method",
                                    modelClass.toString()));
        }
    }

    private Class<?> extractModelClass(Class<?> mapperClass) {
        Type[] types = mapperClass.getGenericInterfaces();
        ParameterizedType target = null;
        for (Type type : types) {
            if (type instanceof ParameterizedType
                    && ((ParameterizedType) type).getRawType().equals(
                    AutoMapper.class)) {
                target = (ParameterizedType) type;
                break;
            }
        }
        Type[] parameters = target.getActualTypeArguments();
        Class<?> modelClass = (Class<?>) parameters[0];
        return modelClass;
    }

    private void injectInsertSql(boolean batch, Class<?> mapperClass,
                                 Class<?> modelClass, TableInfo table) {

        KeyGenerator keyGenerator = new NoKeyGenerator();
        StringBuilder fieldBuilder = new StringBuilder();
        StringBuilder placeholderBuilder = new StringBuilder();
        SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
        if (batch) {
            sqlMethod = SqlMethod.INSERT_BATCH;
        }
        fieldBuilder
                .append("\n<trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">\n");
        placeholderBuilder
                .append("\n<trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">\n");
        String keyProperty = null;
        String keyColumn = null;
        if (table.getIdType() == IdType.AUTO) {

            keyGenerator = new Jdbc3KeyGenerator();
            keyProperty = table.getKeyProperty();
            keyColumn = table.getKeyColumn();
        } else {
            fieldBuilder.append(table.getKeyColumn()).append(",");
            placeholderBuilder.append("#{").append(batch ? "item." : "");
            placeholderBuilder.append(table.getKeyProperty()).append("},");
        }
        List<TableFieldInfo> fieldList = table.getFieldList();
        for (TableFieldInfo fieldInfo : fieldList) {
            if (!batch) {
                fieldBuilder.append("\n\t<if test=\"")
                        .append(fieldInfo.getProperty()).append("!=null\">");
                placeholderBuilder.append("\n\t<if test=\"")
                        .append(fieldInfo.getProperty()).append("!=null\">");
            }
            fieldBuilder.append(fieldInfo.getColumn()).append(",");
            placeholderBuilder.append("#{").append(batch ? "item." : "")
                    .append(fieldInfo.getProperty()).append("},");
            if (!batch) {
                fieldBuilder.append("</if>");
                placeholderBuilder.append("</if>");
            }
        }
        fieldBuilder.append("\n</trim>");
        placeholderBuilder.append("\n</trim>");
        String sql = String.format(sqlMethod.getSql(), table.getTableName(),
                fieldBuilder.toString(), placeholderBuilder.toString());
        SqlSource sqlSource = languageDriver.createSqlSource(configuration,
                sql, modelClass);
        this.addInsertMappedStatement(mapperClass, modelClass,
                sqlMethod.getMethod(), sqlSource, keyGenerator, keyProperty,
                keyColumn);
    }

    private void injectDeleteSelectiveSql(Class<?> mapperClass,
                                          Class<?> modelClass, TableInfo table) {
        SqlMethod sqlMethod = SqlMethod.DELETE_SELECTIVE;
        String sql = String.format(sqlMethod.getSql(), table.getTableName(),
                sqlWhere(table));
        SqlSource sqlSource = languageDriver.createSqlSource(configuration,
                sql, modelClass);
        this.addMappedStatement(mapperClass, sqlMethod, sqlSource,
                SqlCommandType.DELETE, null);
    }

    private void injectDeleteSql(boolean batch, Class<?> mapperClass,
                                 Class<?> modelClass, TableInfo table) {
        SqlMethod sqlMethod = SqlMethod.DELETE_BY_ID;
        SqlSource sqlSource = null;
        if (batch) {
            sqlMethod = SqlMethod.DELETE_BATCH;
            StringBuilder ids = new StringBuilder();
            ids.append("\n<foreach item=\"item\" index=\"index\" collection=\"list\" separator=\",\">");
            ids.append("#{item}");
            ids.append("\n</foreach>");
            String sql = String.format(sqlMethod.getSql(),
                    table.getTableName(), table.getKeyColumn(), ids.toString());
            sqlSource = languageDriver.createSqlSource(configuration, sql,
                    modelClass);
        } else {
            String sql = String.format(sqlMethod.getSql(),
                    table.getTableName(), table.getKeyColumn(),
                    table.getKeyColumn());
            sqlSource = new RawSqlSource(configuration, sql, Object.class);
        }
        this.addMappedStatement(mapperClass, sqlMethod, sqlSource,
                SqlCommandType.DELETE, null);
    }

    private void injectUpdateSql(Class<?> mapperClass, Class<?> modelClass,
                                 TableInfo table) {
        SqlMethod sqlMethod = SqlMethod.UPDATE_BY_ID;
        StringBuilder set = new StringBuilder();
        set.append("<trim prefix=\"SET\" suffixOverrides=\",\" suffix=\"WHERE ");
        set.append(table.getKeyColumn()).append("=#{")
                .append(table.getKeyProperty()).append("}\">");
        List<TableFieldInfo> fieldList = table.getFieldList();
        for (TableFieldInfo fieldInfo : fieldList) {
            set.append("\n<if test=\"").append(fieldInfo.getProperty())
                    .append("!=null\">\n");
            set.append(fieldInfo.getColumn()).append("=#{")
                    .append(fieldInfo.getProperty()).append("},");
            set.append("\n</if>");
        }
        set.append("\n</trim>");
        String sql = String.format(sqlMethod.getSql(), table.getTableName(),
                set.toString());
        SqlSource sqlSource = languageDriver.createSqlSource(configuration,
                sql, modelClass);
        this.addUpdateMappedStatement(mapperClass, modelClass,
                sqlMethod.getMethod(), sqlSource);
    }

    private void injectSelectSql(boolean batch, Class<?> mapperClass,
                                 Class<?> modelClass, TableInfo table) {
        SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
        SqlSource sqlSource = null;
        if (batch) {
            sqlMethod = SqlMethod.SELECT_BATCH;
            StringBuilder ids = new StringBuilder();
            ids.append("\n<foreach item=\"item\" index=\"index\" collection=\"list\" separator=\",\">");
            ids.append("#{item}");
            ids.append("\n</foreach>");
            sqlSource = languageDriver.createSqlSource(configuration, String
                    .format(sqlMethod.getSql(), sqlSelectColumns(table),
                            table.getTableName(), table.getKeyColumn(),
                            ids.toString()), modelClass);
        } else {
            sqlSource = new RawSqlSource(configuration, String.format(
                    sqlMethod.getSql(), sqlSelectColumns(table),
                    table.getTableName(), table.getKeyColumn(),
                    table.getKeyProperty()), Object.class);
        }
        this.addMappedStatement(mapperClass, sqlMethod, sqlSource,
                SqlCommandType.SELECT, modelClass);
    }

    private void injectSelectOneSql(Class<?> mapperClass, Class<?> modelClass,
                                    TableInfo table) {
        SqlMethod sqlMethod = SqlMethod.SELECT_ONE;
        String sql = String.format(sqlMethod.getSql(), sqlSelectColumns(table),
                table.getTableName(), sqlWhere(table));
        SqlSource sqlSource = languageDriver.createSqlSource(configuration,
                sql, modelClass);
        this.addMappedStatement(mapperClass, sqlMethod, sqlSource,
                SqlCommandType.SELECT, modelClass);
    }

    private void injectSelectListSql(Class<?> mapperClass, Class<?> modelClass,
                                     TableInfo table) {
        SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
        StringBuilder where = new StringBuilder("\n<if test=\"ew!=null\">");
        where.append("\n<if test=\"ew.entity!=null\">\n<where>");
        where.append("\n<if test=\"ew.entity.").append(table.getKeyProperty())
                .append("!=null\">\n");
        where.append(table.getKeyColumn()).append("=#{ew.entity.")
                .append(table.getKeyProperty()).append("}");
        where.append("\n</if>");
        List<TableFieldInfo> fieldList = table.getFieldList();
        for (TableFieldInfo fieldInfo : fieldList) {
            where.append("\n<if test=\"ew.entity.")
                    .append(fieldInfo.getProperty()).append("!=null\">\n");
            where.append(" AND ").append(fieldInfo.getColumn())
                    .append("=#{ew.entity.").append(fieldInfo.getProperty())
                    .append("}");
            where.append("\n</if>");
        }
        where.append("\n</where>\n</if>");
        where.append("\n<if test=\"ew.orderByField!=null\">\n${ew.orderByField}\n</if>");
        where.append("\n</if>");
        String sql = String.format(sqlMethod.getSql(), sqlSelectColumns(table),
                table.getTableName(), where.toString());
        SqlSource sqlSource = languageDriver.createSqlSource(configuration,
                sql, modelClass);
        this.addMappedStatement(mapperClass, sqlMethod, sqlSource,
                SqlCommandType.SELECT, modelClass);
    }

    private String sqlSelectColumns(TableInfo table) {
        StringBuilder columns = new StringBuilder();
        if (table.isKeyRelated()) {
            columns.append(table.getKeyColumn()).append(" AS ")
                    .append(table.getKeyProperty());
        } else {
            columns.append(table.getKeyProperty());
        }
        List<TableFieldInfo> fieldList = table.getFieldList();
        for (TableFieldInfo fieldInfo : fieldList) {
            columns.append(",").append(fieldInfo.getColumn());
            if (fieldInfo.isRelated()) {
                columns.append(" AS ").append(fieldInfo.getProperty());
            }
        }
        return columns.toString();
    }

    private String sqlWhere(TableInfo table) {
        StringBuilder where = new StringBuilder();
        where.append("\n<where>");
        where.append("\n<if test=\"").append(table.getKeyProperty())
                .append("!=null\">\n");
        where.append(table.getKeyColumn()).append("=#{")
                .append(table.getKeyProperty()).append("}");
        where.append("\n</if>");
        List<TableFieldInfo> fieldList = table.getFieldList();
        for (TableFieldInfo fieldInfo : fieldList) {
            where.append("\n<if test=\"").append(fieldInfo.getProperty())
                    .append("!=null\">\n");
            where.append(" AND ").append(fieldInfo.getColumn()).append("=#{")
                    .append(fieldInfo.getProperty()).append("}");
            where.append("\n</if>");
        }
        where.append("\n</where>");
        return where.toString();
    }

    private MappedStatement addMappedStatement(Class<?> mapperClass,
                                               SqlMethod sm, SqlSource sqlSource, SqlCommandType sqlCommandType,
                                               Class<?> resultType) {
        return this.addMappedStatement(mapperClass, sm.getMethod(), sqlSource,
                sqlCommandType, null, resultType, new NoKeyGenerator(), null,
                null);
    }

    private MappedStatement addInsertMappedStatement(Class<?> mapperClass,
                                                     Class<?> modelClass, String id, SqlSource sqlSource,
                                                     KeyGenerator keyGenerator, String keyProperty, String keyColumn) {
        return this.addMappedStatement(mapperClass, id, sqlSource,
                SqlCommandType.INSERT, modelClass, null, keyGenerator,
                keyProperty, keyColumn);
    }

    private MappedStatement addUpdateMappedStatement(Class<?> mapperClass,
                                                     Class<?> modelClass, String id, SqlSource sqlSource) {
        return this.addMappedStatement(mapperClass, id, sqlSource,
                SqlCommandType.UPDATE, modelClass, null, new NoKeyGenerator(),
                null, null);
    }

    private MappedStatement addMappedStatement(Class<?> mapperClass, String id,
                                               SqlSource sqlSource, SqlCommandType sqlCommandType,
                                               Class<?> parameterClass, Class<?> resultType,
                                               KeyGenerator keyGenerator, String keyProperty, String keyColumn) {
        String statementName = mapperClass.getName() + "." + id;
        if (configuration.hasStatement(statementName)) {
            System.err
                    .println("{"
                            + statementName
                            + "} Has been loaded by XML or SqlProvider, ignoring the injection of the SQL.");
            return null;
        }
        return assistant.addMappedStatement(id, sqlSource,
                StatementType.PREPARED, sqlCommandType, null, null, null,
                parameterClass, null, resultType, null, false, true, false,
                keyGenerator, keyProperty, keyColumn,
                configuration.getDatabaseId(), new MybatisXMLLanguageDriver(),
                null);
    }

}
